package org.finesys.common.security.authentication.base;

import java.security.Principal;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;

import org.finesys.common.security.core.constant.OAuth2ErrorCodesExpand;
import org.finesys.common.security.core.exception.ScopeException;
import org.finesys.common.security.core.util.OAuth2ConfigurerUtils;
import org.finesys.common.security.core.util.OAuth2EndpointUtils;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.security.oauth2.core.OAuth2ErrorCodes;
import org.springframework.security.oauth2.core.OAuth2RefreshToken;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AccessTokenAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.context.AuthorizationServerContextHolder;
import org.springframework.security.oauth2.server.authorization.token.DefaultOAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;

import cn.hutool.extra.spring.SpringUtil;
import lombok.extern.slf4j.Slf4j;

/**
 * 处理自定义授权
 */
@Slf4j
public abstract class OAuth2ResourceOwnerBaseAuthenticationProvider<T extends OAuth2ResourceOwnerBaseAuthenticationToken> implements AuthenticationProvider {


    private static final String ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1";

    private final OAuth2AuthorizationService oAuth2AuthorizationService;

    private final OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator;

    private final AuthenticationManager authenticationManager;

    private final MessageSourceAccessor messages;


    public OAuth2ResourceOwnerBaseAuthenticationProvider(HttpSecurity httpSecurity) {
        this.oAuth2AuthorizationService = httpSecurity.getSharedObject(OAuth2AuthorizationService.class);
        this.tokenGenerator = OAuth2ConfigurerUtils.getTokenGenerator(httpSecurity);
        this.authenticationManager = httpSecurity.getSharedObject(AuthenticationManager.class);
        Assert.notNull(tokenGenerator, "tokenGenerator cannot be null");
        Assert.notNull(oAuth2AuthorizationService, "authorizationService cannot be null");
        Assert.notNull(authenticationManager, "authenticationManager cannot be null");
        // 国际化配置
        this.messages = new MessageSourceAccessor(SpringUtil.getBean("securityMessageSource"), Locale.CHINA);
    }

    /**
     * 创建token
     */
    public abstract UsernamePasswordAuthenticationToken buildToken(Authentication authentication, Map<String, Object> requestParameters);

    /**
     * 当前provider是否支持此令牌类型
     *
     * @param authentication
     * @return
     */
    @Override
    public abstract boolean supports(Class<?> authentication);

    /**
     * 当前的请求客户端是否支持此模式
     *
     * @param registeredClient
     */
    public abstract void checkClient(RegisteredClient registeredClient);

    /**
     * 国际化对象
     */
    public MessageSourceAccessor getMessageSourceAccessor() {
        return this.messages;
    }

    /**
     * Performs authentication with the same contract as
     * {@link AuthenticationManager#authenticate(Authentication)} .
     *
     * @param authentication the authentication request object.
     * @return a fully authenticated object including credentials. May return
     * <code>null</code> if the <code>AuthenticationProvider</code> is unable to support
     * authentication of the passed <code>Authentication</code> object. In such a case,
     * the next <code>AuthenticationProvider</code> that supports the presented
     * <code>Authentication</code> class will be tried.
     * @throws AuthenticationException if authentication fails.
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        T resourceOwnerBaseAuthentication = (T) authentication;
        //获取客户端认证对象
        OAuth2ClientAuthenticationToken clientPrincipal = OAuth2EndpointUtils.getAuthenticatedClient(resourceOwnerBaseAuthentication);
        //获取注册客户端
        RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
        //检验客户端，具体实现由子类实现处理
        checkClient(registeredClient);
        //权限
        Set<String> authorizedScopes = null;
        if (!CollectionUtils.isEmpty(resourceOwnerBaseAuthentication.getScopes())) {
            for (String scope : resourceOwnerBaseAuthentication.getScopes()) {
                if (registeredClient!=null&&!registeredClient.getScopes().contains(scope)) {
                    throw new OAuth2AuthenticationException(OAuth2ErrorCodes.INVALID_CLIENT);
                }
            }
            authorizedScopes = new HashSet<>(resourceOwnerBaseAuthentication.getScopes());
        } else {
            throw new ScopeException("scope_is_empty");
        }
        //扩展参数
        Map<String, Object> reqParameters = resourceOwnerBaseAuthentication.getAdditionalParams();
        try {
            //构建token
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = buildToken(resourceOwnerBaseAuthentication, reqParameters);
            Authentication usernamePasswordAuthentication = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
            //TokenContext
            DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder().registeredClient(registeredClient)
                    .principal(usernamePasswordAuthentication)
                    .authorizationServerContext(AuthorizationServerContextHolder.getContext())
                    .authorizedScopes(authorizedScopes)
                    .authorizationGrantType(resourceOwnerBaseAuthentication.getAuthorizationGrantType())
                    .authorizationGrant(resourceOwnerBaseAuthentication);
            //authorizationBuilder
            OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization.withRegisteredClient(registeredClient)
                    .principalName(usernamePasswordAuthentication.getName())
                    .authorizationGrantType(resourceOwnerBaseAuthentication.getAuthorizationGrantType())
                    .authorizedScopes(authorizedScopes);

            //Access token
            OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
            OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
            if (generatedAccessToken == null) {
                OAuth2Error oAuth2Error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the access token.", ERROR_URI);
                throw new OAuth2AuthenticationException(oAuth2Error);
            }
            OAuth2AccessToken auth2AccessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER, generatedAccessToken.getTokenValue()
                    , generatedAccessToken.getIssuedAt(), generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());

            if (generatedAccessToken instanceof ClaimAccessor) {
                authorizationBuilder.id(auth2AccessToken.getTokenValue()).token(auth2AccessToken, (meta) ->
                                meta.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME, ((ClaimAccessor) generatedAccessToken).getClaims()))
                        .authorizedScopes(authorizedScopes)
                        .attribute(Principal.class.getName(), usernamePasswordAuthentication);
            } else {
                authorizationBuilder.id(auth2AccessToken.getTokenValue()).accessToken(auth2AccessToken);
            }
            //Refresh Token
            OAuth2RefreshToken oAuth2RefreshToken = null;
            if (registeredClient!=null&&registeredClient.getAuthorizationGrantTypes().contains(AuthorizationGrantType.REFRESH_TOKEN) &&
                    !clientPrincipal.getClientAuthenticationMethod().equals(ClientAuthenticationMethod.NONE)) {
                tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.REFRESH_TOKEN).build();
                OAuth2Token generateRefreshToken = tokenGenerator.generate(tokenContext);
                if (generateRefreshToken == null) {
                    OAuth2Error oAuth2Error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, "The token generator failed to generate the refresh token.", ERROR_URI);
                    throw new OAuth2AuthenticationException(oAuth2Error);
                }
                oAuth2RefreshToken = (OAuth2RefreshToken) generateRefreshToken;
                authorizationBuilder.refreshToken(oAuth2RefreshToken);
            }
            OAuth2Authorization authorization = authorizationBuilder.build();
            this.oAuth2AuthorizationService.save(authorization);
            log.debug("returning OAuth2AccessTokenAuthenticationToken");
            return new OAuth2AccessTokenAuthenticationToken(registeredClient, clientPrincipal, auth2AccessToken, oAuth2RefreshToken,
                    Objects.requireNonNull(authorization.getAccessToken().getClaims()));
        } catch (AuthenticationException e) {
            throw oAuth2AuthenticationException(resourceOwnerBaseAuthentication, e);
        }
    }

    /**
     * 异常信息抛出
     */
    private AuthenticationException oAuth2AuthenticationException(Authentication authentication, AuthenticationException authenticationException) {
        if (authenticationException instanceof UsernameNotFoundException) {
            return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USERNAME_NOT_FOUND));
        }
        if (authenticationException instanceof BadCredentialsException) {
            return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.BAD_CREDENTIALS));
        }
        if (authenticationException instanceof LockedException) {
            return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_LOCKED));
        }
        if (authenticationException instanceof DisabledException) {
            return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_DISABLE));
        }
        if (authenticationException instanceof AccountExpiredException) {
            return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.USER_EXPIRED));
        }
        if (authenticationException instanceof CredentialsExpiredException) {
            return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodesExpand.CREDENTIALS_EXPIRED));
        }
        if (authenticationException instanceof ScopeException) {
            return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_SCOPE));
        }
        if (log.isErrorEnabled()) {
            log.error(authenticationException.getLocalizedMessage());
        }
        return new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR), authenticationException.getLocalizedMessage(), authenticationException);
    }
}
